/* SelectWidget.js
 *
 * Copyright 2006, Tim Dwelle.
 *
 * Licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See
 * the License for the specific language governing
 * permissions and limitations under the License.
 *
 */

$package("dowry.widget");

/**
 * Object represenation of a UI widget that knows how to
 * handle single-entry select boxes.
 *
 */
$class("SelectWidget").$extends("Widget").$with("Transformable").$as(
{
    /**
     * Mapping of of select box values to the actual object
     * values, by the 'value' of the option.
     *
     */
    _values : null,

    /**
     * Property indicating whether the select widget should
     * return a reference to the same value used to add the
     * item to the list.  This is sometimes useful when a
     * select is populated by an object, and you wish to
     * pull back the entire object graph.  The default is
     * false.
     *
     */
    byReference : false,

    /**
     * Property indicating the string name of the field on
     * the object where the label goes.  The default is
     * 'name'.
     *
     */
    label : "name",

    /**
     * Property indicating the string name of the field on
     * the object where the option value goes.  The default
     * is null, indicating that the object *is* the option
     * value.
     *
     */
    option : null,

    /**
     * Construct a new instance of SelectWidget.  Any values
     * pre-populated in this select widget at the time of
     * instantiation will be treated as "prompt" values (not
     * real select options... simply descriptive text like
     * <code>"Pick one..."</code>).
     *
     * @param ele    the element (or ID of the element)
     *               to bind to this datatype
     *
     */
    SelectWidget : function(ele)
    {
        this.$super(arguments);

        // create our values map
        this._values = new Object();

        ele = this.getElement();
        if (ele)
        {
            // apparently, a select box can't have a title
            // (ie. the tooltip)... so when we prepare, we
            // will put an annonymous span around the
            // select... but even this is *far* from
            // perfect...
            var span = this._createEnclosingSpan(ele);

            // any options currently on the select list
            // are flagged to be ignored... by adding
            // the property "prompt"
            for (var i=0; i < ele.options.length; i++)
            {
                var opt = ele.options[i];
                opt.prompt = true;
            }
        }
    },

    /**
     * Adds the specified values to the list.
     *
     * <p>
     * The option created will have a value determined based
     * on the bound element's datatype.  The label for the
     * option will be determined based on the field of each
     * element specified by the <code>label</code> property.
     * </p>
     *
     * @param vals  an array of values that are type
     *              appropriate for the element's datatype
     *
     */
    addValues : function(vals)
    {
        vals = this.transform(vals);
        var ele = this.getElement();

        // is our datatype working on the vals
        // array (such as ArrayType) or the
        // elements of the array?  we'll try
        // converting the array and if we don't
        // get suitable results, we will assume
        // that we need to convert at the element
        // level...
        var convertElems = false;
        var dt = this.getDatatype();
        if (dt && vals != null && vals.join)
        {
            // well, at least we know we have
            // a datatype to convert!
            var convertElems = true;

            var convertedArr = dt.toFormattedString(val);
            if (convertedArr != null && convertedArr.join)
            {
                // got back an array... assume
                // our elements are good!
                vals = convertedArr;
                convertElems = false;
            }
        }

        if (ele && vals != null)
        {
            for (var i=0; i < vals.length; i++)
            {
                var val = this.transformValue(vals[i]);
                var txt = this.getLabel(val);

                // convert the raw value
                if (convertElems)
                {
                    val = dt.toFormattedString(val);
                }

                // add to values map
                if (this.byReference)
                {
                    this._values[val] = vals[i];
                }

                // can't have a select option
                // with a null value... use an
                // empty string instead
                if (val == null)
                {
                    val = "";
                }

                // if no label specified, use the
                // value for both
                if (txt == null)
                {
                    txt = val;
                }

                // add the option
                var opt = new Option(txt, val);
                ele.options[ele.options.length] = opt;
            }
        }
    },

    /**
     * Determines the element's label based on the value
     * of the object field specified via the
     * <code>label</code> property.
     *
     * @param val  the single value to get the label for
     *
     * @return     the label for this value
     *
     */
    getLabel : function(val)
    {
        var label = null;

        if (val != null)
        {
            label = val[this.label];
        }

        return label;
    },

    /**
     * Gets the value of the bound element, represented as a
     * type appropriate for the element's datatype.
     *
     * @return  the value
     *
     */
    getValue : function()
    {
        var val = null;

        var ele = this.getElement();
        if (ele && !ele.disabled && ele.selectedIndex > -1)
        {
            var opt = ele.options[ele.selectedIndex];

            // only get the value if the selected
            // option isn't a read-only prompt
            if (opt &&
                (opt.prompt == null || !opt.prompt))
            {
                var optVal = opt.value;

                // try to lookup value from values map
                if (this.byReference && this._values[optVal])
                {
                    val = this._values[optVal];
                }

                // convert the raw value
                else
                {
                    var dt = this.getDatatype();
                    if (dt)
                    {
                        val = dt.toDatatype(optVal);
                    }
                }
            }
        }

        return val;
    },

    /**
     * Removes all values from the list.
     *
     */
    removeAllValues : function()
    {
        var ele = this.getElement();
        if (ele)
        {
            // need to make sure we only delete
            // non-prompts... this requires us to
            // set the option to null to remove it
            // from the options array, but this has
            // a nasty side-effect that it also
            // changes the length of the array we
            // are looping through (thus the
            // backwards iterating)
            for (var i=ele.options.length-1; i > -1; i--)
            {
                var opt = ele.options[i];
                if (opt.prompt == null || !opt.prompt)
                {
                    ele.options[i] = null;
                }
            }

            // reset our values map
            this._values = new Object();
        }
    },

    /**
     * Sets the error message on the widget.  If a non-null
     * error message is provided, this will make the UI
     * field display as <code>invalid</code> with the
     * message appearing in the tooltip.  A null message
     * will remove any error currently being displayed.
     *
     * @param  msg   the message
     *
     */
    setErrorMessage : function(msg)
    {
        var ele = this.getElement();

        // update both the element
        // *and* its enclosing span
        if (ele && ele.parentNode)
        {
            this._setErrorMessageImpl(ele.parentNode, msg);
        }

        this.$super(arguments);
    },

    /**
     * Sets the value of the bound element, using a type
     * appropriate for the element's datatype.
     *
     * @param val  the value to set
     *
     */
    setValue : function(val)
    {
        var ele = this.getElement();
        if (ele)
        {
            // convert the provided value
            var dt = this.getDatatype();
            if (dt)
            {
                val = dt.toFormattedString(val);
            }

            // can't have a select option
            // with a null value... use an
            // empty string instead
            if (val == null)
            {
                val = "";
            }

            for (var i = 0; i < ele.options.length; i++)
            {
                var opt = ele.options[i];
                opt.selected = (opt.value == val);
            }
        }
    },

    /**
     * When the SelectWidget is started, associate an
     * event to validate the selection.
     *
     */
    start : function()
    {
        var ele = this.getElement();
        if (ele)
        {
            this._addAsteriskIfRequired(ele.parentNode, true);
            Dowry.event.addEvent(ele, "change", this.validate);
        }
    }
});
